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

Drag and Drop Animation CSS: 9 Physics Patterns

Nine drag and drop animation css builds: A magnetic grid catches the icon inside a 24px radius, a rubber-band edge lets it go once the stretch passes a limit,

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

Drag and Drop Animation CSS: How the Motion Keeps Going After You Let Go

All nine builds below answer the same question in a different way: what should the dragged element do in the instant between the pointer moving and the pointer letting go? Item 01 only reacts while the pointer is still down, snapping toward a cell the moment the distance crosses a threshold. By item 03 that same in-progress reaction is capped by a limit instead of running freely, item 05 and item 06 keep something moving after the release itself, and item 09 folds three of the earlier mechanisms — grid magnet, rubber boundary, momentum fling — onto a single board. The order moves from a value that only matters while the pointer is down toward a value that keeps deciding the motion well after the hand is gone.

01Grid Magnet That Snaps an Icon Into the Nearest Cell

updateMagnetHighlight() measures the live distance from the dragged icon to every grid cell's center on each pointer move, and once that distance drops under GRID_MAGNET_SNAP_PX the nearest cell is marked is-target while the icon's position blends toward that cell by a pull factor scaled from the remaining distance. Releasing inside that radius lets settleTick() spring the icon the rest of the way into the cell instead of teleporting it there, using SPRING_STIFFNESS and SPRING_DAMPING to shape the pull.

Grid-Magnet Drag That Pulls an Item Into a Cell Within Range
Open in a new tab

01 · Grid-Magnet Drag That Pulls an Item Into a Cell Within Range — Measured area 14.062% · moving frames 20/23Original by GODRICH · SCSS and React ship in the zip · See the password

GRID_MAGNET_SNAP_PXSPRING_STIFFNESSSPRING_DAMPING
  function updateMagnetHighlight(rawX, rawY) {
    var near = nearestCell();
    cells.forEach(function (c) { c.classList.remove('is-target'); });
    if (near.dist < GRID_MAGNET_SNAP_PX) {
      near.cell.classList.add('is-target');
      targetCell = near;
      var pull = Math.max(0, 1 - near.dist / GRID_MAGNET_SNAP_PX) * 0.55;
      pos.x = rawX + (near.offset.x - rawX) * pull;
      pos.y = rawY + (near.offset.y - rawY) * pull;
    } else {
      targetCell = null;
      pos.x = rawX; pos.y = rawY;
    }
    applyPos();
  }

  var raf = 0, lastTs = null;
  function settleTick(ts) {
    if (lastTs === null) lastTs = ts;

Item 01 repainted 14.1% of the frame at an average intensity of 177.0 across 20/23 measured frames.

02Elastic Tether That Stretches and Snaps Back to Its Origin

drawTether() recalculates the line connecting the icon to its origin on every frame from the two elements' live bounding rectangles, rotating a single div to match the distance and angle between them. That same distance also drives the line's thickness through TETHER_THICKNESS_BASE_PX, and on release settleTick() springs the icon back toward the origin with TETHER_SPRING_STIFFNESS and TETHER_SPRING_DAMPING until the line's length reaches zero.

Elastic-Tether Drag That Stretches as If Roped to Its Origin
Open in a new tab

02 · Elastic-Tether Drag That Stretches as If Roped to Its Origin — Measured area 9.738% · moving frames 22/23Original by GODRICH · SCSS and React ship in the zip · See the password

TETHER_SPRING_STIFFNESSTETHER_SPRING_DAMPINGTETHER_THICKNESS_BASE_PX
  function drawTether() {
    var or = origin.getBoundingClientRect();
    var ir = icon.getBoundingClientRect();
    var ox = or.left + or.width / 2, oy = or.top + or.height / 2;
    var ix = ir.left + ir.width / 2, iy = ir.top + ir.height / 2;
    var dx = ix - ox, dy = iy - oy;
    var length = Math.hypot(dx, dy);
    var angle = Math.atan2(dy, dx) * 180 / Math.PI;
    var thickness = Math.max(1.5, TETHER_THICKNESS_BASE_PX - length * 0.012);
    var boardRect = root.getBoundingClientRect();
    line.style.left = (ox - boardRect.left) + 'px';
    line.style.top = (oy - boardRect.top) + 'px';
    line.style.width = length + 'px';
    line.style.height = thickness + 'px';
    line.style.transform = 'rotate(' + angle + 'deg)';
    line.style.opacity = length < 2 ? '0' : '1';
  }

  function settleTick(ts) {
    if (lastTs === null) lastTs = ts;
    var dt = Math.min((ts - lastTs) / 1000, 0.032);
    lastTs = ts;
    var fx = (0 - pos.x) * TETHER_SPRING_STIFFNESS - vel.x * TETHER_SPRING_DAMPING;

Item 02 repainted 9.7% of the frame at an average intensity of 39.6 across 22/23 measured frames.

03Rubber Boundary That Squishes an Icon at the Edge

overflowOf() measures how far the pointer's coordinates have crossed the container's edge, and applyVisual() turns that overflow into a ratio capped at BOUNDARY_RUBBER_MAX_PX that stretches the icon's scale and skews it in the direction it's being pulled. Letting go beyond the edge springs the icon back inside using BOUNDARY_SPRING_STIFFNESS and BOUNDARY_SPRING_DAMPING, so the same two numbers that shape the stretch also shape the snap back.

Rubber Boundary That Stretches and Snaps Back Past the Edge
Open in a new tab

03 · Rubber Boundary That Stretches and Snaps Back Past the Edge — Measured area 10.521% · moving frames 20/23Original by GODRICH · SCSS and React ship in the zip · See the password

BOUNDARY_RUBBER_MAX_PXBOUNDARY_SPRING_STIFFNESSBOUNDARY_SPRING_DAMPING
  function overflowOf(clientX, clientY) {
    var b = bounds();
    var ox = 0, oy = 0;
    if (clientX < b.minX) ox = clientX - b.minX;
    else if (clientX > b.maxX) ox = clientX - b.maxX;
    if (clientY < b.minY) oy = clientY - b.minY;
    else if (clientY > b.maxY) oy = clientY - b.maxY;
    return { x: ox, y: oy };
  }

  function applyVisual(clampedX, clampedY, overflow) {
    var ratioX = Math.min(1, Math.abs(overflow.x) / BOUNDARY_RUBBER_MAX_PX);
    var ratioY = Math.min(1, Math.abs(overflow.y) / BOUNDARY_RUBBER_MAX_PX);
    var ratio = Math.max(ratioX, ratioY);
    var scaleStretch = 1 + ratio * 0.28;
    var scaleSquash = 1 - ratio * 0.22;
    var skewX = overflow.x !== 0 ? (overflow.x > 0 ? 1 : -1) * ratioX * 6 : 0;
    var skewY = overflow.y !== 0 ? (overflow.y > 0 ? 1 : -1) * ratioY * 6 : 0;
    var sx = ratioX >= ratioY ? scaleStretch : scaleSquash;
    var sy = ratioY >= ratioX ? scaleStretch : scaleSquash;
    icon.style.transform = 'translate(' + clampedX.toFixed(1) + 'px,' + clampedY.toFixed(1) + 'px) scale(' + sx.toFixed(3) + ',' + sy.toFixed(3) + ') skew(' + skewX.toFixed(2) + 'deg,' + skewY.toFixed(2) + 'deg)';
    root.classList.toggle('is-stretch', ratio > 0.02);
  }

Item 03 repainted 10.5% of the frame at an average intensity of 99.0 across 20/23 measured frames.

04Neighbor Swap That Trades Places at the Midpoint

moveDrag() recomputes the dragged chip's on-screen center every pointer move and rounds it to the nearest slot, and while that slot differs from the chip's current one, moveInOrder() swaps the two positions in the underlying order array. Every swap calls retarget() and kickSpring(), which drive the untouched chips into their new slots through SWAP_FLIP_STIFFNESS and SWAP_FLIP_DAMPING rather than snapping them there instantly.

Swap Drag That Trades Places Once It Crosses a Neighbor's Midpoint
Open in a new tab

04 · Swap Drag That Trades Places Once It Crosses a Neighbor's Midpoint — Measured area 22.487% · moving frames 17/23Original by GODRICH · SCSS and React ship in the zip · See the password

SWAP_FLIP_STIFFNESSSWAP_FLIP_DAMPINGKEY_STEP_SLOTS
    var chipRect = dragChip.getBoundingClientRect();
    var trackRect = track.getBoundingClientRect();
    var centerX = chipRect.left + chipRect.width / 2 - trackRect.left;
    var targetSlot = Math.max(0, Math.min(chips.length - 1, Math.round((centerX - STEP / 2) / STEP)));
    while (targetSlot !== slotOf(DRAGGED_ID)) {
      var from = slotOf(DRAGGED_ID);
      var step = targetSlot > from ? 1 : -1;
      var to = from + step;
      moveInOrder(from, to);
      retarget();
      kickSpring();
      caption.textContent = 'Swapped places';
    }
    state[DRAGGED_ID].x = rawX;
  }

  function endDrag() {
    dragging = false;
    root.classList.remove('is-dragging');
    caption.textContent = 'Cross the midpoint and it swaps places with its neighbor';

Item 04 repainted 22.5% of the frame at an average intensity of 103.3 across 17/23 measured frames.

05Orbit Handle Fixed to a Radius Around a Hub

angleFromPointer() converts the pointer's live coordinates into a single angle around the hub's fixed center using Math.atan2(), and applyHandle() always places the handle at Math.cos(angle) * ORBIT_RADIUS_PX and the matching sine term, so the radius itself never changes. Releasing the handle hands that angle's last rate of change to inertiaTick(), which multiplies the angular velocity by ORBIT_DECAY_RATE every frame until the spin runs out.

Orbit-Handle Drag Where Only the Angle Around a Center Changes
Open in a new tab

05 · Orbit-Handle Drag Where Only the Angle Around a Center Changes — Measured area 11.255% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

ORBIT_RADIUS_PXORBIT_DECAY_RATEORBIT_KEY_STEP_DEG

  function applyHandle() {
    var x = Math.cos(angle) * ORBIT_RADIUS_PX;
    var y = Math.sin(angle) * ORBIT_RADIUS_PX;
    handle.style.transform = 'translate(' + x.toFixed(1) + 'px,' + y.toFixed(1) + 'px)';
  }

  function angleFromPointer(clientX, clientY) {
    var hr = hub.getBoundingClientRect();
    var hx = hr.left + hr.width / 2, hy = hr.top + hr.height / 2;
    return Math.atan2(clientY - hy, clientX - hx);
  }

  function shortestDelta(a, b) {
    var d = b - a;
    while (d > Math.PI) d -= Math.PI * 2;
    while (d < -Math.PI) d += Math.PI * 2;
    return d;
  }

Item 05 repainted 11.3% of the frame at an average intensity of 186.6 across 23/23 measured frames.

06Momentum Fling That Bounces Off the Walls

flingTick() advances the ball's position by its velocity every frame and reverses whichever velocity component hit a wall the moment the ball's center crosses bounds(), adding the is-bounce class for that frame. The same function multiplies both velocity components by FLING_DECAY_RATE on every frame, so the ball keeps sliding and bouncing until its speed drops under 4 pixels per frame and flingTick() lets it rest.

Momentum-Fling Drag Where Release Speed Pushes the Remaining Distance
Open in a new tab

06 · Momentum-Fling Drag Where Release Speed Pushes the Remaining Distance — Measured area 9.251% · moving frames 17/23Original by GODRICH · SCSS and React ship in the zip · See the password

FLING_DECAY_RATEKEY_IMPULSE_PX_SflingTick
  function flingTick(ts) {
    if (lastTs === null) lastTs = ts;
    var dt = Math.min((ts - lastTs) / 1000, 0.032);
    lastTs = ts;
    pos.x += vel.x * dt;
    pos.y += vel.y * dt;
    var b = bounds();
    var origin = ballOrigin();
    var cx = origin.x + pos.x, cy = origin.y + pos.y;
    if (cx < b.minX) { pos.x += (b.minX - cx); vel.x = Math.abs(vel.x); root.classList.add('is-bounce'); }
    else if (cx > b.maxX) { pos.x -= (cx - b.maxX); vel.x = -Math.abs(vel.x); root.classList.add('is-bounce'); }
    if (cy < b.minY) { pos.y += (b.minY - cy); vel.y = Math.abs(vel.y); root.classList.add('is-bounce'); }
    else if (cy > b.maxY) { pos.y -= (cy - b.maxY); vel.y = -Math.abs(vel.y); root.classList.add('is-bounce'); }
    vel.x *= Math.pow(FLING_DECAY_RATE, dt * 60);
    vel.y *= Math.pow(FLING_DECAY_RATE, dt * 60);
    applyBall();
    var speed = Math.hypot(vel.x, vel.y);
    if (speed > 4) { raf = requestAnimationFrame(flingTick); return; }
    vel.x = 0; vel.y = 0;
    root.classList.remove('is-bounce');

Item 06 repainted 9.3% of the frame at an average intensity of 55.6 across 17/23 measured frames.

07Trash Zone That Shakes Before It Deletes

shakeTick() turns how long the icon has overlapped the trash zone into a single dwellMs value, and scales a sine-and-cosine shake offset by that time relative to TRASH_DWELL_SHAKE_MS, capped at TRASH_SHAKE_MAX_PX. Once dwellMs passes TRASH_DELETE_DWELL_MS the caption switches to warn that releasing now deletes the icon, so the same timer both shakes the icon and decides when a release counts as a delete.

Trash-Shake Warning That Trembles Over the Delete Zone
Open in a new tab

07 · Trash-Shake Warning That Trembles Over the Delete Zone — Measured area 17.508% · moving frames 17/23Original by GODRICH · SCSS and React ship in the zip · See the password

TRASH_DWELL_SHAKE_MSTRASH_SHAKE_MAX_PXTRASH_DELETE_DWELL_MS
  function shakeTick(ts) {
    if (deleted) return;
    var isOver = overlapsTrash();
    if (isOver && !overTrash) { dwellStart = ts; }
    overTrash = isOver;
    trash.classList.toggle('is-armed', overTrash);
    var shakeX = 0, shakeY = 0;
    var dwellMs = 0;
    if (overTrash && dwellStart !== null) {
      dwellMs = ts - dwellStart;
      var amp = Math.min(1, dwellMs / TRASH_DWELL_SHAKE_MS) * TRASH_SHAKE_MAX_PX;
      shakeX = Math.sin(ts / 40) * amp;
      shakeY = Math.cos(ts / 55) * amp * 0.6;
      if (dwellMs > TRASH_DELETE_DWELL_MS) {
        caption.textContent = 'Release now and it deletes';
        root.classList.add('is-armed');
      } else {
        caption.textContent = 'Stay longer and it gets ready to delete';
        root.classList.remove('is-armed');
      }
    } else {
      caption.textContent = dragging ? 'Try moving it over the trash' : 'Stay over the trash too long and it shakes, then disappears';
      root.classList.remove('is-armed');
    }
    applyIcon(shakeX, shakeY, 1);

Item 07 repainted 17.5% of the frame at an average intensity of 115.5 across 17/23 measured frames.

08Lasso Select That Picks Up Whatever It Crosses

drawLasso() redraws a rectangle from the drag's starting point to the current pointer position every frame, and once the drag passes LASSO_MIN_DRAG_PX, updateLassoSelection() calls rectsIntersect() against every card's live bounding rectangle to decide which ones are selected. The field itself carries role="listbox", so a keyboard user can also focus one card at a time and press Enter to toggle it without ever dragging a rectangle.

Lasso-Select Drag That Picks Only What's Inside the Dragged Rectangle
Open in a new tab

08 · Lasso-Select Drag That Picks Only What's Inside the Dragged Rectangle — Measured area 14.549% · moving frames 17/23Original by GODRICH · SCSS and React ship in the zip · See the password

LASSO_MIN_DRAG_PXrectsIntersectrole="listbox"

  function rectsIntersect(a, b) {
    return a.left < b.right && a.right > b.left && a.top < b.bottom && a.bottom > b.top;
  }

  function updateLassoSelection(rect) {
    items.forEach(function (el, idx) {
      var r = el.getBoundingClientRect();
      setSelected(idx, rectsIntersect(rect, r));
    });
  }

  function drawLasso(x0, y0, x1, y1) {
    var left = Math.min(x0, x1), top = Math.min(y0, y1);
    var w = Math.abs(x1 - x0), h = Math.abs(y1 - y0);
    var fieldRect = field.getBoundingClientRect();
    lasso.style.left = (left - fieldRect.left) + 'px';
    lasso.style.top = (top - fieldRect.top) + 'px';
    lasso.style.width = w + 'px';
    lasso.style.height = h + 'px';
    lasso.style.opacity = '1';

Item 08 repainted 14.5% of the frame at an average intensity of 74.3 across 17/23 measured frames.

09Hero Board Scene Combining Magnet, Rubber, and Fling

The same drag handler checks nearestCell() against HERO_GRID_MAGNET_SNAP_PX first, and only when nothing is close enough does it fall back to clamping the pointer's overflow past the board edge by HERO_BOUNDARY_RUBBER_MAX_PX and squishing the icon's scale. Releasing outside a magnet cell hands the icon to the same fling loop as item 06, decayed here by HERO_FLING_DECAY_RATE instead of FLING_DECAY_RATE, so one board runs all three mechanisms from a single pointer handler.

Hero Scene Combining Several Physics Patterns on One Board
Open in a new tab

09 · Hero Scene Combining Several Physics Patterns on One Board — Measured area 8.088% · moving frames 18/23Original by GODRICH · SCSS and React ship in the zip · See the password

HERO_GRID_MAGNET_SNAP_PXHERO_BOUNDARY_RUBBER_MAX_PXHERO_FLING_DECAY_RATE
    var near = nearestCell(cx, cy);
    cells.forEach(function (c) { c.classList.remove('is-target'); });

    if (near && near.dist < HERO_GRID_MAGNET_SNAP_PX) {
      near.cell.classList.add('is-target');
      var pull = Math.max(0, 1 - near.dist / HERO_GRID_MAGNET_SNAP_PX) * 0.6;
      pos.x = rawX + ((near.x - o.x) - rawX) * pull;
      pos.y = rawY + ((near.y - o.y) - rawY) * pull;
      applyIcon(1, 1);
      caption.textContent = 'Snapping to the magnet cell';
    } else {
      var b = boardBounds();
      var overX = 0, overY = 0;
      if (cx < b.minX) overX = cx - b.minX; else if (cx > b.maxX) overX = cx - b.maxX;
      if (cy < b.minY) overY = cy - b.minY; else if (cy > b.maxY) overY = cy - b.maxY;
      if (overX !== 0 || overY !== 0) {
        var clampedX = Math.max(-HERO_BOUNDARY_RUBBER_MAX_PX, Math.min(HERO_BOUNDARY_RUBBER_MAX_PX, overX));
        var clampedY = Math.max(-HERO_BOUNDARY_RUBBER_MAX_PX, Math.min(HERO_BOUNDARY_RUBBER_MAX_PX, overY));
        pos.x = rawX - (overX - clampedX);
        pos.y = rawY - (overY - clampedY);
        var ratio = Math.max(Math.abs(clampedX), Math.abs(clampedY)) / HERO_BOUNDARY_RUBBER_MAX_PX;
        applyIcon(1 + ratio * 0.22, 1 - ratio * 0.18);
        caption.textContent = 'Squishing like rubber at the boundary';

Item 09 repainted 8.1% of the frame at an average intensity of 125.5 across 18/23 measured frames.

Which of the Nine Fits Your Drag Interaction

Every drag surface needs some way to decide what happens between pickup and drop, but which of these nine earns its place depends on what's actually being dragged. Item 01's grid magnet suits an icon that always belongs in exactly one cell, like a home-screen layout, and item 03's rubber boundary fits a card meant to stay inside a fixed play area no matter how hard it's pulled. Item 04's midpoint swap is the shape a reorderable list needs, and a companion build with its own drop-index math lives at Drag to Reorder UI: 9 Lists, One Drop Index. Item 07's trash dwell timer belongs next to any drop target that deletes rather than places, and a file-upload variant of a computed drop target sits in Drag and Drop File Upload UI: 9 Computed Dropzones. Item 05's fixed-radius orbit is the closest of these nine to a value dragged around a circle rather than across a plane, which CSS Radial Menu: 9 Circular Control Patterns covers from the dial side instead of the drag side, and every drag and drop interaction ui built here still needs the same drag and drop ui examples question answered first: what happens the instant the pointer stops moving.

Nine Drag and Drop Physics Patterns, Measured in Area and Intensity

Cumulative area is the share of each fixed-camera frame that actually repainted, averaged across every measured frame, and intensity is the average pixel delta where it did — most of the nine below sit under 25% because only a small icon, chip, or ball is actually moving against a mostly static card.

| Pattern | Cumulative area | Avg. intensity | Frames moved |

|---|---|---|---|

| 01 Grid magnet snap | 14.1% | 177.0 | 20/23 |

| 02 Elastic tether | 9.7% | 39.6 | 22/23 |

| 03 Boundary rubber squish | 10.5% | 99.0 | 20/23 |

| 04 Neighbor swap | 22.5% | 103.3 | 17/23 |

| 05 Orbit handle | 11.3% | 186.6 | 23/23 |

| 06 Momentum fling | 9.3% | 55.6 | 17/23 |

| 07 Trash zone shake | 17.5% | 115.5 | 17/23 |

| 08 Lasso select | 14.5% | 74.3 | 17/23 |

| 09 Hero board scene | 8.1% | 125.5 | 18/23 |

Three of the nine — 02, 06, and 09 — sit under 10% cumulative area, and that's expected rather than a flaw: a tether line, a small ball, and one icon on a hero board simply don't cover much of a fixed-camera frame no matter how the physics underneath them behaves.

Where the Value Changes the Reaction — Magnet Snap Distance, Rubber-Band Max Stretch, Decay Rate

Three numbers below do most of the deciding: GRID_MAGNET_SNAP_PX sets how close item 01's icon has to get to a cell's center before that cell starts pulling it in, BOUNDARY_RUBBER_MAX_PX caps how far item 03's icon can stretch past the container edge before the rubber effect stops growing, and FLING_DECAY_RATE sets how much of item 06's release velocity survives from one frame to the next. None of the three change what shape draws — a magnet target, a stretched icon, a bouncing ball — only how far the pointer has to travel before that shape reacts and how long the motion keeps going once the pointer is gone.

Where It Breaks — Touch-Scroll Conflicts, Out-of-Bounds Coordinates, Keyboard Alternatives

All nine of these set touch-action: none somewhere, but not on the same scope: eight of them scope it to the single element actually being dragged — the icon, the chip, the handle, the ball — so a touch gesture anywhere else on the card still scrolls the page normally, while item 08 sets it on the whole .ls__field, since any point inside that field can start a lasso and none of it should hand the gesture off to the page's own scroll instead. Six of the nine — 01, 02, 04, 05, 07, and 08 — never clamp the pointer-derived position against a bounds() or boardBounds() function, so a pointer that keeps moving after it leaves the demo card can carry the icon, chip, or lasso rectangle well outside the visible frame with nothing pulling it back until release; only 03, 06, and 09 define that clamp explicitly. The keyboard alternative doesn't depend on any of that: a keydown handler on the draggable element itself grabs on Enter, steps with the arrow keys, and drops on Enter again on all nine, so losing the pointer or dragging past the edge never means losing the interaction — the nine index.html/style.scss pairs behind these builds, dqbvrvq6, are the fastest way to check which of the nine your own board still needs the same bounds clamp added to.

Three Variants and Accessibility

| Variant | Value change | Feel |

|---|---|---|

| Adjust the magnetic grid snap distance | 01 GRID_MAGNET_SNAP_PX 24 → 8 | Raising the value pulls the icon into a cell from farther away, while lowering it means the icon must sit almost exactly over the cell before it snaps. |

| Adjust the rubber-band boundary's maximum stretch | 03 BOUNDARY_RUBBER_MAX_PX 40 → 90 | Lowering the value makes the edge stop and snap back after only a short pull, while raising it lets the drag stretch much farther before it stops and snaps back. |

| Adjust the momentum fling decay rate | 06 FLING_DECAY_RATE 0.92 → 0.98 | Lowering the value stops the item within a few frames of release, while raising it lets it keep sliding on the release speed for much longer. |

None of the nine demos announce a selection or a snap through aria-live — a screen reader gets no separate narration of when an icon locks into a cell or a card joins the lasso selection beyond whatever state change its own markup exposes. All nine check prefers-reduced-motion through the same matchMedia('(prefers-reduced-motion: reduce)') call, and eight of them use it twice: once to skip the idle attract loop that plays before a visitor's first drag, and once inside the physics itself, jumping straight to the resting position instead of running the spring, fling, or shake. Item 08's lasso has no release physics to skip, so its single check only covers the idle attract loop; it's also the only one of the nine that calls focus({ preventScroll: true }), so tabbing to a card inside the selectable list never scrolls the demo card out of view first. Pointer Events, requestAnimationFrame(), prefers-reduced-motion, and transform are documented on MDN.

Can Drag and Drop Animation CSS Work Without a Library?

All nine of these run on the Pointer Events API — pointerdown, pointermove, pointerup, and pointercancel — instead of the older HTML5 drag-and-drop API's dragstart, dragover, and drop. That single event family already unifies mouse, touch, and pen input, so none of the nine reaches for a drag and drop animation maker or any other library to normalize the input source or to run the spring, fling, or magnet math; every calculation above runs on plain numbers inside requestAnimationFrame(). A drag and drop interaction ui built this way stays a handful of drag and drop ui examples short of a full library, and that gap is exactly what a search for drag and drop animation free usually turns up first.

Questions to Settle Before You Wire Up Drag and Drop Animation CSS

Is any of this built on the HTML5 drag-and-drop API?

No — all nine listen for pointerdown, pointermove, and pointerup/pointercancel on a real element instead of dragstart/dragover/drop, and none of the nine sets draggable="true" or reads DataTransfer. That keeps the same code path working for mouse, touch, and pen input without a second implementation.

Can every one of these nine be operated without a pointer at all?

Yes — all nine attach a keydown handler to the draggable element itself, following the same pattern of pressing Enter to grab, the arrow keys to move, and Enter again to drop. Only item 08's lasso adds a second path on top of that, since its role="listbox" field also lets a focused card be toggled with Enter one at a time.

Which of the nine repaints the least of the frame, and why?

Item 09's hero scene sits lowest at 8.1% cumulative area, just under item 06's fling at 9.3% and item 02's tether at 9.7% — moving a single small icon or ball only repaints the pixels immediately around it, unlike item 04's swap, which redraws every chip in the row at once and reaches 22.5%.

Enter the archive password

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